查看原文
其他

【caffe速成】caffe图像分类从模型自定义到测试

言有三 有三AI 2019-12-26

这是给大家准备的caffe速成例子

言有三


毕业于中国科学院,计算机视觉方向从业者,有三工作室等创始人

作者 | 言有三(微信号Longlongtogo)

编辑 | 言有三


这一次我们讲讲 Caffe 这个主流的开源框架从训练到测试出结果的全流程。到此,我必须假设大家已经有了深度学习的基础知识并了解卷积网络的工作原理。


相关的代码、数据都在我们 Git 上,希望大家 Follow 一下这个 Git 项目,后面会持续更新不同框架下的任务。


https://github.com/longpeng2008/LongPeng_ML_Course


这一篇我们说一个分类任务,给大家准备了 500 张微笑的图片、500 张非微笑的图片,放置在 data 目录下,图片预览如下,已经缩放到 60*60 的大小:

这是非微笑的图片:

这是微笑的图片:

01


Caffe 是什么

Caffe 是以 C++/CUDA 代码为主,最早的深度学习框架之一,比 TensorFlow、Mxnet、Pytorch 等都更早,支持命令行、Python 和 Matlab 接口,单机多卡、多机多卡等都可以很方便的使用,CPU 和 GPU 之间无缝切换。


对于入门级别的任务,如图像分类,Caffe 上手的成本最低,几乎不需要写一行代码就可以开始训练,所以我推荐 Caffe 作为入门学习的框架。


Caffe 相对于 TensorFlow 等使用 pip 一键安装的方式来说,编译安装稍微麻烦一些,但其实依旧很简单,我们以 Ubuntu 16.04 为例子,官网的安装脚本足够用了,它有一些依赖库。


sudo apt-get install libprotobuf-dev libleveldb-dev libsnappy-dev libopencv-dev libhdf5-serial-dev protobuf-compiler
sudo apt-get install --no-install-recommends libboost-all-devsudo apt-get install libatlas-base-dev
sudo apt-get install libgflags-dev libgoogle-glog-dev liblmdb-dev

装完之后,到 Git 上 clone 代码,修改 Makefile.config 就可以进行编译安装,如果其中有任何问题,多 Google,还有什么问题,留言吧。当然,对于有 GPU 的读者,还需要安装 cuda 以及 Nvidia 驱动。

02


Caffe 训练

Caffe 完成一个训练,必要准备以下资料:一个是 train.prototxt 作为网络配置文件,另一个是 solver.prototxt 作为优化参数配置文件,再一个是训练文件 list。

另外,在大多数情况下,需要一个预训练模型作为权重的初始化。


(1)准备网络配置文件

我们准备了一个 3*3 的卷积神经网络,它的 train.prototxt 文件是这样的:

name: "mouth"
layer {
 name: "data"
 type: "ImageData"
 top: "data"
 top: "clc-label"
 image_data_param {
   source: "all_shuffle_train.txt"
   batch_size: 96
   shuffle: true
 }
 transform_param {
   mean_value: 104.008
   mean_value: 116.669
   mean_value: 122.675
   crop_size: 48
   mirror: true
 }
 include: { phase: TRAIN}
}
layer {
 name: "data"
 type: "ImageData"
 top: "data"
 top: "clc-label"
 image_data_param {
   source: "all_shuffle_val.txt"
   batch_size: 30
   shuffle: false
 }
 transform_param {
   mean_value: 104.008
   mean_value: 116.669
   mean_value: 122.675
   crop_size: 48
   mirror: false
 }
 include: { phase: TEST}
}
layer {
 name: "conv1"
 type: "Convolution"
 bottom: "data"
 top: "conv1"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 convolution_param {
   num_output: 12
   pad: 1
   kernel_size: 3
   stride: 2
   weight_filler {
     type: "xavier"
     std: 0.01
   }
   bias_filler {
     type: "constant"
     value: 0.2
   }
 }
}
layer {
 name: "relu1"
 type: "ReLU"
 bottom: "conv1"
 top: "conv1"
}
layer {
 name: "conv2"
 type: "Convolution"
 bottom: "conv1"
 top: "conv2"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 convolution_param {
   num_output: 20
   kernel_size: 3
   stride: 2
   pad: 1
   weight_filler {
     type: "xavier"
     std: 0.1
   }
   bias_filler {
     type: "constant"
     value: 0.2
   }
 }
}
layer {
 name: "relu2"
 type: "ReLU"
 bottom: "conv2"
 top: "conv2"
}
layer {
 name: "conv3"
 type: "Convolution"
 bottom: "conv2"
 top: "conv3"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 convolution_param {
   num_output: 40
   kernel_size: 3
   stride: 2
   pad: 1
   weight_filler {
     type: "xavier"
     std: 0.1
   }
   bias_filler {
     type: "constant"
     value: 0.2
   }
 }
}
layer {
 name: "relu3"
 type: "ReLU"
 bottom: "conv3"
 top: "conv3"
}
layer {
 name: "ip1-mouth"
 type: "InnerProduct"
 bottom: "conv3"
 top: "pool-mouth"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 inner_product_param {
   num_output: 128
   weight_filler {
     type: "xavier"
   }
   bias_filler {
     type: "constant"
     value: 0
   }
 }
}

layer {
   bottom: "pool-mouth"
   top: "fc-mouth"
   name: "fc-mouth"
   type: "InnerProduct"
   param {
       lr_mult: 1
       decay_mult: 1
   }
   param {
       lr_mult: 2
       decay_mult: 1
   }
   inner_product_param {
       num_output: 2
       weight_filler {
           type: "xavier"
       }
       bias_filler {
           type: "constant"
           value: 0
       }
   }
}
layer {
   bottom: "fc-mouth"
   bottom: "clc-label"
   name: "loss"
   type: "SoftmaxWithLoss"
   top: "loss"
}
layer {
   bottom: "fc-mouth"
   bottom: "clc-label"
   top: "acc"
   name: "acc"
   type: "Accuracy"
   include {
       phase: TRAIN
   }
   include {
       phase: TEST
   }
}

可以看出,Caffe 的这个网络配置文件,每一个卷积层,都是以 layer{} 的形式定义,layer 的bottom、top 就是它的输入输出,type 就是它的类型,有的是数据层、有的是卷积层、有的是 loss 层。

我们采用 netscope 来可视化一下这个模型。

从上面看很直观的看到,网络的输入层是 data 层,后面接了3个卷积层,其中每一个卷积层都后接了一个 relu 层,最后 ip1-mouth、fc-mouth 是全连接层。Loss 和 acc 分别是计算 loss 和 acc 的层。


各层的配置有一些参数,比如 conv1 有卷积核的学习率、卷积核的大小、输出通道数、初始化方法等,这些可以后续详细了解。


(2)准备训练 list

我们看上面的 data layer,可以到 

image_data_param 里面有

source: "all_shuffle_train.txt"

它是什么呢,就是输入用于训练的 list,它的内容是这样的:

../../../../datas/mouth/1/182smile.jpg 1

../../../../datas/mouth/1/435smile.jpg 1

../../../../datas/mouth/0/40neutral.jpg 0

../../../../datas/mouth/1/206smile.jpg 1

../../../../datas/mouth/0/458neutral.jpg 0

../../../../datas/mouth/0/158neutral.jpg 0

../../../../datas/mouth/1/322smile.jpg 1

../../../../datas/mouth/1/83smile.jpg 1

../../../../datas/mouth/0/403neutral.jpg 0

../../../../datas/mouth/1/425smile.jpg 1

../../../../datas/mouth/1/180smile.jpg 1

../../../../datas/mouth/1/233smile.jpg 1

../../../../datas/mouth/1/213smile.jpg 1

../../../../datas/mouth/1/144smile.jpg 1

../../../../datas/mouth/0/327neutral.jpg 0

格式就是,图片的名字 + 空格 + label,这就是 Caffe 用于图片分类默认的输入格式。


(3)准备优化配置文件:

net: "./train.prototxt"

test_iter: 100

test_interval: 10

base_lr: 0.00001

momentum: 0.9

type: "Adam"

lr_policy: "fixed"

display: 100

max_iter: 10000

snapshot: 2000

snapshot_prefix: "./snaps/conv3_finetune"

solver_mode: GPU

介绍一下上面的参数。

net 是网络的配置路径。test_interval是指训练迭代多少次之后,进行一次测试。test_iter是测试多少个batch,如果它等于 1,就说明只取一个 batchsize 的数据来做测试,如果 batchsize 太小,那么对于分类任务来说统计出来的指标也不可信,所以最好一次测试,用到所有测试数据。因为,常令test_iter*test_batchsize=测试集合的大小。

base_lr、momentum、type、lr_policy是和学习率有关的参数,base_lr和lr_policy决定了学习率大小如何变化。type 是优化的方法,以后再谈。max_iter是最大的迭代次数,snapshot 是每迭代多少次之后存储迭代结果,snapshot_prefix为存储结果的目录,caffe 存储的模型后缀是 .caffemodel。solver_mode可以指定用 GPU 或者 CPU 进行训练。


(4)训练与结果可视化

我们利用 C++ 的接口进行训练,命令如下:

SOLVER=./solver.prototxt

WEIGHTS=./init.caffemodel

../../../../libs/Caffe_Long/build/tools/caffe train -solver $SOLVER -weights $WEIGHTS -gpu 0 2>&1 | tee log.txt

其中,caffe train 就是指定训练。我们可以利用脚本可视化一下训练结果,具体参考git项目:

03


Caffe 测试

上面我们得到了训练结果,下面开始采用自己的图片进行测试。


train.prototxt 与 test.prototxt 的区别


训练时的网络配置与测试时的网络配置是不同的,测试没有 acc 层,也没有 loss 层,取输出的 softmax 就是分类的结果。同时,输入层的格式也有出入,不需要再输入 label,也不需要指定图片 list,但是要指定输入尺度,我们看一下 test.prototxt 和可视化结果。

name: "mouth"
layer {
 name: "data"
 type: "Input"
 top: "data"
 input_param { shape: { dim: 1 dim: 3 dim: 48 dim: 48 } }
}

layer {
 name: "conv1"
 type: "Convolution"
 bottom: "data"
 top: "conv1"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 convolution_param {
   num_output: 12
   pad: 1
   kernel_size: 3
   stride: 2
   weight_filler {
     type: "xavier"
     std: 0.01
   }
   bias_filler {
     type: "constant"
     value: 0.2
   }
 }
}
layer {
 name: "relu1"
 type: "ReLU"
 bottom: "conv1"
 top: "conv1"
}
layer {
 name: "conv2"
 type: "Convolution"
 bottom: "conv1"
 top: "conv2"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 convolution_param {
   num_output: 20
   kernel_size: 3
   stride: 2
   pad: 1
   weight_filler {
     type: "xavier"
     std: 0.1
   }
   bias_filler {
     type: "constant"
     value: 0.2
   }
 }
}
layer {
 name: "relu2"
 type: "ReLU"
 bottom: "conv2"
 top: "conv2"
}
layer {
 name: "conv3"
 type: "Convolution"
 bottom: "conv2"
 top: "conv3"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 convolution_param {
   num_output: 40
   kernel_size: 3
   stride: 2
   pad: 1
   weight_filler {
     type: "xavier"
     std: 0.1
   }
   bias_filler {
     type: "constant"
     value: 0.2
   }
 }
}
layer {
 name: "relu3"
 type: "ReLU"
 bottom: "conv3"
 top: "conv3"
}
layer {
 name: "ip1-mouth"
 type: "InnerProduct"
 bottom: "conv3"
 top: "pool-mouth"
 param {
   lr_mult: 1
   decay_mult: 1
 }
 param {
   lr_mult: 2
   decay_mult: 0
 }
 inner_product_param {
   num_output: 128
   weight_filler {
     type: "xavier"
   }
   bias_filler {
     type: "constant"
     value: 0
   }
 }
}
layer {
   bottom: "pool-mouth"
   top: "fc-mouth"
   name: "fc-mouth"
   type: "InnerProduct"
   param {
       lr_mult: 1
       decay_mult: 1
   }
   param {
       lr_mult: 2
       decay_mult: 1
   }
   inner_product_param {
       num_output: 2
       weight_filler {
           type: "xavier"
       }
       bias_filler {
           type: "constant"
           value: 0
       }
   }
}
layer {
   bottom: "fc-mouth"
   name: "loss"
   type: "Softmax"
   top: "prob"
}

使用 Python 进行测试

由于 Python 目前广泛使用,下面使用 Python 来进行测试,它要做的就是导入模型、导入图片、输出结果。

下面是所有的代码,我们详细解释下:

---代码段1,这一段,我导入一些基本库,同时导入caffe的路径---

#_*_ coding:utf8

import sys

sys.path.insert(0, '../../../../../libs/Caffe_Long/python/')

import caffe

import os,shutil

import numpy as np

from PIL import Image as PILImage

from PIL import ImageMath

import matplotlib.pyplot as plt

import time

import cv2


---代码段2,这一段,我们添加一个参数解释器,方便参数管理---

debug=True

import argparse

def parse_args():

   parser = argparse.ArgumentParser(description='test resnet model for portrait segmentation')

   parser.add_argument('--model', dest='model_proto', help='the model', default='test.prototxt', type=str)

   parser.add_argument('--weights', dest='model_weight', help='the weights', default='./test.caffemodel', type=str)

   parser.add_argument('--testsize', dest='testsize', help='inference size', default=60,type=int)

   parser.add_argument('--src', dest='img_folder', help='the src image folder', type=str, default='./')

   parser.add_argument('--gt', dest='gt', help='the gt', type=int, default=0)

   args = parser.parse_args()

   return args


def start_test(model_proto,model_weight,img_folder,testsize):

---代码段3,这一段,我们就完成了网络的初始化---

   caffe.set_device(0)

   #caffe.set_mode_cpu()

   net = caffe.Net(model_proto, model_weight, caffe.TEST)

   imgs = os.listdir(img_folder)


   pos = 0

   neg = 0


   for imgname in imgs:

---代码段4,这一段,是读取图片并进行预处理,还记得我们之前的训练,是采用 BGR 的输入格式,减去了图像均值吧,同时,输入网络的图像,也需要 resize 到相应尺度。预处理是通过 caffe 的类,transformer 来完成,set_mean 完成均值,set_transpose 完成维度的替换,因为 caffe blob 的格式是 batch、channel、height、width,而 numpy 图像的维度是 height、width、channel 的顺序---


      imgtype = imgname.split('.')[-1]

      imgid = imgname.split('.')[0]

      if imgtype != 'png' and imgtype != 'jpg' and imgtype != 'JPG' and imgtype != 'jpeg' and imgtype != 'tif' and imgtype != 'bmp':

          print imgtype,"error"

          continue

      imgpath = os.path.join(img_folder,imgname)


      img = cv2.imread(imgpath)

      if img is None:

          print "---------img is empty---------",imgpath

          continue


      img = cv2.resize(img,(testsize,testsize))


      transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape})

      transformer.set_mean('data', np.array([104.008,116.669,122.675]))

      transformer.set_transpose('data', (2,0,1))


---代码段5,这一段,就得到了输出结果了,并做一些可视化显示---

      out = net.forward_all(data=np.asarray([transformer.preprocess('data', img)]))


      result = out['prob'][0]

      print "---------result prob---------",result,"-------result size--------",result.shape

      probneutral = result[0]

      print "prob neutral",probneutral 


      probsmile = result[1]

      print "prob smile",probsmile

      problabel = -1

      probstr = 'none'

      if probneutral > probsmile:

          probstr = "neutral:"+str(probneutral)

          pos = pos + 1

      else:

          probstr = "smile:"+str(probsmile)

          neg = neg + 1


      if debug:

         showimg = cv2.resize(img,(256,256))

         cv2.putText(showimg,probstr,(30,50),cv2.FONT_HERSHEY_SIMPLEX,1,(0,0,255),1)

         cv2.imshow("test",showimg)

         k = cv2.waitKey(0)

         if k == ord('q'):

             break


   print "pos=",pos 

   print "neg=",neg 


if __name__ == '__main__':

    args = parse_args()

    start_test(args.model_proto,args.model_weight,args.img_folder,args.testsize)


经过前面的介绍,我们已经学会了 Caffe 的基本使用,但是我们不能停留于此。Caffe 是一个非常优秀的开源框架,有必要去细读它的源代码。

至于怎么读 Caffe 的代码,建议阅读我写的Caffe代码阅读系列内容。

04


总结

虽然现在很多人没有从 Caffe 开始学,但是希望提升自己 C++ 水平和更深刻理解深度学习中的一些源码的,建议从 Caffe 开始学起。


更多,欢迎到知乎专栏去投稿与交流,配套资料将放出在github,可扫描二维码进入。


打一个小广告,我的计算机视觉公开课《AI 图像识别项目从入门到上线》上线了,将讲述从零基础到完成一个实际的项目到微信小程序上线的整个流程,欢迎交流捧场。





如果想加入我们,后台留言吧

微信

Longlongtogo

公众号内容

1 图像基础|2 深度学习|3 行业信息

往期精选

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存